﻿
using System;
using System.Collections.Generic;
using EFCachingProvider.Caching;
using Sop;

namespace EFCachingProvider.ScalableCache
{
    /// <summary>
    /// Implementation of <see cref="ICache"/> which uses SOP's data store
    /// as cache repository.
    /// SOP's data store keeps most recently used objects in-memory and 
    /// uses disk to extend capacity for those not fitted in-memory, for scale.
    /// </summary>
    public partial class ScalableCache : ICache
    {
        private readonly CacheManager _cacheManager = new CacheManager();

        public ScalableCache(TimeSpan? commitInterval = null, 
            int expireEntriesIntervalInSec = 20,
            string storeFilename = null, 
            bool memoryExtenderMode = false,
            int expireBatchCount = 500)
        {
            _memoryExtenderMode = memoryExtenderMode;
            ProcessEntriesIntervalInSec = expireEntriesIntervalInSec;
            CommitInterval = commitInterval;
            if (!string.IsNullOrEmpty(storeFilename))
                StoreFilename = storeFilename;
            ExpireBatchCount = expireBatchCount;
        }

        /// <summary>
        /// Tries to the get cached entry by key.
        /// </summary>
        /// <param name="key">The cache key.</param>
        /// <param name="value">The retrieved value.</param>
        /// <returns>A value of <c>true</c> if entry was found in the cache, <c>false</c> otherwise.</returns>
        public bool GetItem(string key, out object value)
        {
            MethodOnIdle();

            // check if item is in in-memory cache...
            bool foundButExpired;
            if (_cacheManager.GetItem(key, out value, out foundButExpired))
                return true;
            // just return false if found but expired...
            if (foundButExpired) return false;

            // check if item is on disk cache...
            CacheEntry entry;
            var now = GetCurrentDate();
            value = null;
            Entities.Locker.Lock(OperationType.Write);
            bool r = Entities.TryGetValue(key, out entry);
            Entities.Locker.Unlock(OperationType.Write);
            if (r)
            {
                value = entry.Value;
            }
            if (r)
            {
                if (now >= entry.ExpirationTime)
                {
                    // just return false if item expired, background processor will do eviction
                    return false;
                }
                if (!_cacheManager.CacheItem(key, entry)) return false;
            }
            return r;
        }

        /// <summary>
        /// Adds the specified entry to the cache.
        /// </summary>
        /// <param name="key">The entry key.</param>
        /// <param name="value">The entry value.</param>
        /// <param name="dependentEntitySets">The list of dependent entity sets.</param>
        /// <param name="slidingExpiration">The sliding expiration.</param>
        /// <param name="absoluteExpiration">The absolute expiration.</param>
        public void PutItem(string key, object value,
                            IEnumerable<string> dependentEntitySets,
                            TimeSpan slidingExpiration, DateTime absoluteExpiration)
        {
            MethodOnIdle();
            _cacheManager.PutItem(key, value, dependentEntitySets, slidingExpiration, absoluteExpiration);
        }
        /// <summary>
        /// Invalidates all cache entries which are dependent on any of the specified entity sets.
        /// </summary>
        /// <param name="entitySets">The entity sets.</param>
        public void InvalidateSets(IEnumerable<string> entitySets)
        {
            MethodOnIdle();
            if (entitySets == null)
                throw new ArgumentNullException("entitySets");

            _cacheManager.InvalidateSets(entitySets);
        }
        /// <summary>
        /// Invalidates cache entry with a given key.
        /// </summary>
        /// <param name="key">The cache key.</param>
        public void InvalidateItem(string key)
        {
            MethodOnIdle();
            _cacheManager.InvalidateItem(key);
        }


        #region SOP Data Store specific
        /// <summary>
        /// Store Filename.
        /// </summary>
        public string StoreFilename
        {
            get { return _storeFilename; }
            private set
            {
                if (string.IsNullOrEmpty(value))
                    throw new ArgumentNullException("value");
                _storeFilename = value;
            }
        }

        private string _storeFilename = string.Format("{0}\\{1}", Environment.CurrentDirectory, DefaultFilenameLiteral);
        private const string DefaultFilenameLiteral = "EFCacheStore.dta";

        /// <summary>
        /// Process Entries Interval In Seconds.
        /// </summary>
        public int ProcessEntriesIntervalInSec { get; private set; }

        public IObjectServer Server
        {
            get
            {
                if (_server == null)
                {
                    lock (_serverLocker)
                    {
                        if (_server != null) return _server;
                        _server = ObjectServer.OpenWithTransaction(_storeFilename,
                            new Preferences { MemoryExtenderMode = _memoryExtenderMode });
                    }
                }
                return _server;
            }
        }
        private IObjectServer _server;
        private bool _memoryExtenderMode;

        private ISortedDictionary<string, CacheEntry> Entities
        {
            get
            {
                if (_entities == null)
                {
                    lock (_serverLocker)
                    {
                        if (_entities != null && !_entities.IsDisposed) return _entities;
                        // Create store Factory
                        var storeFactory = new StoreFactory();
                        // create entity cache data store...
                        _entities = storeFactory.Get<string, CacheEntry>(Server.SystemFile.Store,
                                                                            "EFCacheStore", mruManaged: false);
                    }
                }
                return _entities;
            }
        }
        private ISortedDictionary<string, CacheEntry> _entities;

        private ISortedDictionary<DateTime, string> EntitiesByDate
        {
            get
            {
                if (_entitiesByDate == null)
                {
                    lock (_serverLocker)
                    {
                        if (_entitiesByDate != null && !_entitiesByDate.IsDisposed) return _entitiesByDate;
                        // Create store Factory
                        var storeFactory = new StoreFactory();
                        // create entity cache data store...
                        _entitiesByDate = storeFactory.Get<DateTime, string>(Server.SystemFile.Store,
                                                                             "EFCacheStoreByDate", mruManaged: false);
                    }
                }
                return _entitiesByDate;
            }
        }
        private ISortedDictionary<DateTime, string> _entitiesByDate;
        #endregion

        /// <summary>
        /// Allows external code to set its method to get date time.
        /// NOTE: this allows unit test code to set date time to some
        /// test driven values.
        /// </summary>
        public Func<DateTime> GetCurrentDate
        {
            get { return _cacheManager.GetCurrentDate; }
            set
            {
                _cacheManager.GetCurrentDate = value;
                if (_lastCommitDateTimeSet) return;
                // set last processing dates if 1st time set of GetCurrentDate...
                _lastCommitDateTimeSet = true;
                _lastCommitDateTime = value();
                _lastProcessExpirationDateTime = value();
            }
        }

        private bool _lastCommitDateTimeSet;
        private DateTime _lastCommitDateTime = DateTime.Now;
        private DateTime _lastProcessExpirationDateTime = DateTime.Now;
    }
}
